摘要

内部类之前我用的不多,但在java类库里却经常看到,是java的常用技术。这章主要围绕内部类和匿名内部类,同时也涉及到“闭包”,“回调”,“lambda表达式”这些常见概念。最后,讨论了内部类常见的使用场景,以及优缺点。

一个最简单的内部类:外部类的一个“备选”组件

这是一个最简单的套嵌结构,Outer是一个外部类,里面有一个Inner内部类。外部类有个方法,创建并返回一个内部类。

class Outer {

    /**
     *  INNER CLASS
     */
    class Inner {
        private Inner() {System.out.println("Hello I am Inner Class!");}
    }

    /**
     *  METHODS
     */
    Inner getInner(){return new Inner();}

    /**
     *  CONSTRUCTOR
     */
    Outer(){System.out.println("Hello I am Outer Class!");}

    /**
     *  FIELDS
     */
}

简单创建一个外部类,然后通过外部类创建内部类。观察初始化顺序。

    public static void main(String[] args) {
        Outer testOuter=new Outer();
        Inner myInner=testOuter.getInner();		//实例化内部类,方法一
		Inner newInner=testOuter.new Inner();	//实例化内部类,方法二
    }
}

//output:
Hello I am Outer Class!
Hello I am Inner Class!
Hello I am Inner Class!

因此得出两个重要事实:

由第一条可以得出,内部类的性质就像外部类的一个“组件”。很类似compositon。但不同在于这个组件不是必须的,而是备选的。平时不需要的时候它不会被初始化,只有必要的时候,才实例化出来使用。

需要当心第二条,想当然的new Outer.Inner()是不能实例化内部类的。因为内部类并不是静态的。因此必须通过外部类的一个实例来实例化,无论是否用new。

另外,在编译层面,Outer类编译完会产生3个class文件。我们以后再探讨这三个文件的具体作用。

Outer$1.class		Outer.class
Outer$Inner.class	Outer.java

内部类是一个“闭包”,自由访问外部类所有字段

第二个实验前,需要指出:内部类和普通类不同,可以用private, protected, public修饰。

第二个实验:主角是外部Outer类的一个字段:字符串info,以及一个方法outerMethod()

class Outer {

    /**
     *  PRIVATE INNER CLASS
     */
    class Inner {
        //直接访问并修改Outer的info字段,能行吗?
        public String toString(){
			info="苍天还没有死";
		}

        //constructor of inner
        Inner() {System.out.println("Hello I am Inner Class!");}
    }

    /**
     *  PUBLIC METHODS
     */
    public Inner getInner(){return new Inner();}
	//显示Outer的info
	public void outerMethod(){System.out.println(info);}

    /**
     *  CONSTRUCTOR
     */
    Outer(String inStr){
        info=inStr;
        System.out.println("Hello I am Outer Class!");
		System.out.println(info);
    }

    /**
     *  PRIVATE FIELDS
     */
    private String info;

}

结果显示,内部Inner类能够访问外部Outer类的所有字段和方法,哪怕是private私有的。不是继承,而是完全指向同一个对象。也就是说内部类完全共享外部类的所有信息,而且能直接对外部类的字段做修改,哪怕是private的字段。 这是因为,在内部类中,会自动被加上了一个指向外部类的引用。所以内部类能够直接对外部类成员字段操作。

    public static void main(String[] args) {
        Outer testOuter=new Outer("苍天已死,黄天当立!");
        Inner myInner=testOuter.getInner();
        myInner.toString();
		myInner.outerMethod();
    }

//output:
Hello I am Outer Class!
苍天已死黄天当立
Hello I am Inner Class!
苍天还没有死

闭包(Closure)

讲到内部类自动包含指向外部类的引用,就引出了“闭包(Closure)”这个概念。

其实闭包的概念非常简单,不严肃地,一句话定义闭包就是,

定义中含有自由变量的函数叫“闭包”。

closure

什么叫“自由变量”? 如上图所示,

f(x)=x+y

函数中,x是约束变量,y就是自由变量。也就是函数的返回值不仅取决于约束变量x,还取决于环境变量y。代码写出来就是下面这样:

int y=50;
add(int x){
	return x+y;
}

所以如果我们单独调用add()函数,是计算不出最后的结果的,因为缺了自由变量y。所以add()函数为了能完成计算,必须随身携带自由变量y,这时候add()函数就可以称为一个“闭包”。这里“闭”的意思其实是封闭,封装了外部自由变量,所以叫“闭包”。

和闭包相对的一个概念叫“组合子(Combinator)”,定义为,

不含有自由变量的函数叫“组合子”。

closure 很简单,如果add()函数有x和y两个参数,不依赖于外部自由变量的话,就是一个组合子。

add(int x, int y){
	return x+y;
}

讲到这里,很容易就看出来,内部类就是一个典型的“闭包”!因为它自带创建它的环境(外部类)的全部变量。

由此引出的一个常用的 “迭代器模式(Interator Pattern)”

根据内部类这两条特性,就引出了每个类库里都会有的迭代器(Iterator)。其实它一点也不神秘, 就是一个简单的内部类,唯一的字段就是一个整型计数器index。就像挂在主序列上的一个指针,表示现在遍历到哪儿了。原理就是利用了内部类自动继承外部类字段的特性,通过访问这个指针,可以直接对外部主序列进行操作。

class Sequence {

    /**
     *  PRIVATE INNER CLASS
     */
    private class Pointer {
        //methods
        public boolean hasNext(){return index!=charSeq.length;}
        public char next(){
            if(index<charSeq.length){index++;}
            return charSeq[index-1];            
        }

        //constructor of inner
        private Pointer() {index=0;}

		//fields
        private int index;	//迭代器的主体“指针”
    }

    /**
     *  PUBLIC METHODS
     */
    public Pointer getPointer(){return new Pointer();}

    /**
     *  PRIVATE CONSTRUCTOR
     */
    private Sequence(String inStr){
        charSeq=inStr.toCharArray();
    }

    /**
     *  PRIVATE FIELDS
     */
    private char[] charSeq;	//主体字符串

}

测试一下,遍历并打印字符串的每一个字符,并用”-“间隔。

    /**
     *  MAIN
     */
    public static void main(String[] args) {
        Sequence testSeq=new Sequence("So an inner class has automatic access to the members of the enclosing class.");
        Pointer testPointer=testSeq.getPointer();
        while(testPointer.hasNext()){
            System.out.print(testPointer.next()+"-");
        }
    }

//output:
S-o- -a-n- -i-n-n-e-r- -c-l-a-s-s- -h-a-s- -a-u-t-o-m-a-t-i-c- -a-c-c-e-s-s- -t-o- -t-h-e- -m-e-m-b-e-r-s- -o-f- -t-h-e- -e-n-c-l-o-s-i-n-g- -c-l-a-s-s-.-

但问题来了,同样的事情,我不用内部类的形式,而是把指针直接作为一个字段放进字符串类照样能实现功能:

//不用内部类,直接用“组合”
class SequenceComp {

    /**
     *  PUBLIC METHODS
     */
    public boolean hasNext(){return index!=charSeq.length;}
    public char next(){
        if(index<charSeq.length){index++;}
        return charSeq[index-1];
    }

    /**
     *  PRIVATE CONSTRUCTOR
     */
    private SequenceComp(String inStr){
        charSeq=inStr.toCharArray();
        index=0;
    }

    /**
     *  PRIVATE FIELDS
     */
    private char[] charSeq;
    private int index;	//迭代器直接作为组件

}

更进一步:外部类对内部类的访问也畅通无阻

实验三在实验二的基础上更进一步,外部类访问内部类私有构造器和私有方法。反过来内部类私有方法再访问外部类的私有字段和方法。

public class Outer {

    /**
     *  PRIVATE INNER CLASS
     */
    private class Inner {
        //methods
        private void privateInnerMethod(){
            privateOuterMethod();
            info="privatedInnerMethod visit info!";
        }
        //constructor of inner
        private Inner() {System.out.println("Hello I am Inner Class!");}
    }

    /**
     *  PUBLIC METHODS
     */
    public Inner getInner(){return new Inner();}
    public void callPrivateInner(){
        Inner theInner=getInner();
        theInner.privateInnerMethod();
        System.out.println(info);
    }
    private void privateOuterMethod(){System.out.println("Private Outer Method visited!");}

    /**
     *  PRIVATE CONSTRUCTOR
     */
    public Outer(String inStr){
        info=inStr;
        System.out.println("Hello I am Outer Class!");
        System.out.println(info);
    }

    /**
     *  PRIVATE FIELDS
     */
    private String info;

从外部另一个包运行这个实验。结果显示,外部类对内部类私有构造器和私有方法都无障碍访问。

    /**
     *  MAIN
     */
    public static void main(String[] args) {
        Outer testOuter=new Outer("Inner class haven't visited me!");
        testOuter.callPrivateInner();
    }
}

//output:
Hello I am Outer Class!
Inner class haven't visited me!
Hello I am Inner Class!
Private Outer Method visited!
privatedInnerMethod visit info!

最后的结果显示,

所以内部类和外部类之间的访问一切畅行无阻。如果外部类是一个姑娘,那内部类就像姑娘颈上的项链,手上的戒指。是附属品也是整体的一部分。可以带,也可以不带。

内部类向上层接口转型,对外隐藏内部实现

外部有个公开接口Destination,private内部类PDestination实现了这个接口。然后只要外部类在实例化内部类的时候向上转型成为公开接口类型。

//外部公开接口
public interface Destination {
	String readLabel();
}

class Parcel {
	private class PContents implements Contents {
		private int i = 11;
		public int value() { return i; }
	}
	//内部类实现外部接口
	protected class PDestination implements Destination {
		private String label;
		private PDestination(String whereTo) {
			label = whereTo;
		}
		public String readLabel() { return label; }
	}
	//关键:外部类实例化内部类的时候向上转型
	public Destination destination(String s) {
		return new PDestination(s);
	}
	public Contents contents() {
		return new PContents();
	}
}

这样做的好处是,在公开类的内部对内部类的访问非常自由,但是从外部类的外面访问内部类的时候,就只能看到公开接口定义的方法。这样的结构非常适合对外隐藏内部实现。

public static void main(String[] args) {
	Parcel4 p = new Parcel4();
	Contents c = p.contents();
	Destination d = p.destination("Tasmania");
}

内部类“回调”外部类,实现多继承

例子很简单,我是一个学生,我的work()函数是写作业。但有一天我接到一个助教的活,可是老师接口也有work()函数。显然作为老师我的work()函数应该是上课。但如果我又想同时保持我写作业的work()函数呢?能不能同时有两个行为不同的work()函数呢?这时候内部类就是个很好的选择。

//老师的work()接口
public interface Teacher{
	public void work();
}

public abstract class Students {

	public void work(){System.out.println("do home work");}

}

class Me extends Students{
	//作为老师的teach()函数已经写好,放在外部类,等待内部类回调
	public void teach(){System.out.println("give lessons");}

	//内部类实现老师接口,work()函数回调外部类teach()函数
	public class TeachAssistant implements Teacher{
		public work(){teach();}
	}
	//返回内部类的引用,向上转型到老师接口
	public Teacher becomeTA{return new TeachAssistant();}

}

class Test {
	public static void main(String[] args){
		Me shen = new Me();
		//作为学生工作
		shen.work();
		//作为助教上课
		Teacher ta = shen.becomeTA();
		ta.work();
	}
//output:
do homework
give lessons
}

我的基本身份是个学生,所以我继承了学生基类,自带的work()函数是做作业。但我同时也具备教课teach()的能力,平时不施展。只有当我的隐藏身份(内部类)“助教”被调用的时候,我的work()函数,回调我的teach()技能。这就是“回调”在java内部类上的应用。

从例子可以看出,利用内部类“回调”,可以让外部类在基本身份之外,有很多不同的新身份,以不同的接口来调用。而且最大的好处就像之前讲的,每个接口只对外暴露接口约定的几个方法,隐藏了内部的其他实现。

回调(Callback)

回调”是很常用的一种技术,回调的方式有很多种,上面讲的内部类只是其中比较常用的一种方式罢了。回调说起来其实很简单,如下图所示,

callback 应用层的程序调用函数库的某一函数,但这个函数说某个具体步骤我不会做,需要应用层告诉我怎么做。所以最好就是应用层写好这个方法,等库函数回调。关于回调其他的形式,以及常见的使用场景可以参考知乎上futeng的一个用心的回答《回调函数(callback)是什么?》。其中提到,匿名内部类的示例才是开源工具中常见到的使用方式。而且回调方法最大的优势在于,异步回调,这样是其最被广为使用的原因。

更重口味:匿名内部类

像我这样的初学者可能会觉得内部类语法很怪,但其实到现在为止的都还属于小清新,Java语法允许的口味要重得多。

首先,内部类可以在外部类的某个成员方法里,简单结构如下:

//外部类
class Outer {
	public void outerMethod(){
		//内部类
		private class Inner{
			public void innerMethod(){}
		}
	}
}

甚至,内部类可以在外部类某成员方法的某个域(花括号)里

//外部类
class Outer {
	public void outerMethod(boolean needInner){
		if(needInner){
			//内部类
			private class Inner{
				public void innerMethod(){}
			}
		}
	}
}

比较常用的是更鬼畜的一种:匿名内部类

//外部接口
public interface Info {
	public printInfo();
}

//外部类
class Outer {
	public Info getInfo(){
		//现场制作并返回匿名内部类
		return new Info(){
			private String info="Hello World";
			public printInfo(){System.out.println(info)};
		};	//冒号不能省
	}
}

最常见的匿名类使用场景,当参数。有的时候参数的类型很不常用,比如只用1,2次,专门写个类文件太麻烦。就可以现场制作这样一个匿名类。只不过,看上去对用户相当不友好呢:>

//外部接口
public interface Info {
	public printInfo();
}

//外部类
class Outer {
	//函数需要一个Info类型的参数
	public Info showNews(Info inputInfo){
		System.out.println("Today's news: ");
		inputInfo.printInfo();
	}

	public static void main(String[] args){
		Outer myOuter=new Outer();
		//要用到Info型的参数的时候,才现场制作
		myOuter.showNews(new Info(){
				private String info="Hello World";
				public printInfo(){System.out.println(info)};
			});
	}
}

匿名内部类从java 8开始能访问非final的参数了

Java 8之前为保护外部类的变量不被破坏,java硬性规定匿名内部类只能访问final类型的外部对象。作者写书的时候Java 8还没有发布。但从Java 8开始,访问非final对象编译器也不会报错了。但匿名内部类内部仍然不能改变这些对象,因为匿名内部类内部会对这些对象做一份拷贝。所有操作都在这个镜像上进行。

Lambda表达式

以匿名内部类作为函数的参数,其实是Lambda表达式的一种形式。Java8中已经正式引入了Lambda表达式。

		//之前的匿名内部类的写法
		myOuter.showNews(new Info(){
				private String info="Hello World";
				public printInfo(){System.out.println(info)};
			});

		//Lambda表达式替代内部匿名类
		myOuter.showNews(()->{
				System.out.println("Hello World!");
			});

}

上面的例子里,看上去使用Lambda表达式的条件还是挺苛刻的。首先Info接口就规定了一个printInfo()方法,还没有成员字段。

λ演算

“Lambda 表达式”(lambda expression)是一个匿名函数,基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。

λ演算到底是个什么鬼?这就要涉及到编程语言最核心的话题了:其实编程语言的本质是关于{逻辑学,语言学,数学}。λ演算是一套用于研究函数定义、函数应用和递归的形式系统。

首先Lambda表达式的基本形式如下:

λ[变量].[表达式]

任何函数都只能带一个参数,如果想表达一个加2函数,

其中的λ就是lambda抽象,代表一个抽象的匿名函数。x是这个函数的唯一参数。x+2代表对函数参数x所做的操作,也就是函数的返回值。

λ演算的函数都只能带一个参数。那想表示带两个或两个以上参数的函数呢?可以用Currying技术:

变量x的函数的返回值本身是另一个函数y。然后调用的环境变量x也被传入了函数y。形成了闭包

其实说到这里,也就理解了为什么匿名内部类被称为Lambda表达式了。除了有“闭包”这样的共同形式之外,其实λ演算还透露了另外一种重要的“函数式编程”思想或者说范式:“一切都是函数”。

实际上λ演算的强大之处在于,它是可以用来表达所有函数的一套形式系统。不但是函数,甚至还有一切我们编程用到的变量,数据结构。具体细节可以看《维基百科-λ演算》的介绍。以及另一篇科普文《编程语言的基石——Lambda calculus》

继承内部类

如果想在外部类的外面,继承内部类,需要给继承类的构造器像这样加一个外部类的引用作为参数。注意继承类内部实际包含的是外部类的基类构造器super()。

class Outer {
	class Inner {}
}
public class InheritInner extends Outer.Inner {
	//! InheritInner() {} // Won’t compile
	InheritInner(Outer o) {
		o.super();
	}
}

继承外部类,内部类不会被覆盖

继承外部类,如果直接再定义一遍内部类,并不会覆盖原有内部类。若想覆盖原有内部类,必须连同外部类一起,同时显式地继承内部类,然后再重写内部类子类的方法。

class Outer {
	class Inner {
		public innerMethod(){System.out.println("Outer.Inner.innerMethod()")}
	}
}
public class InheritOuter extends Outer {
	public class InheritInner extends Outer.Inner {
		public innerMethod(){System.out.println("InheritOuter.InheritInner.innerMethod()")}
	}
}

Why Inner Class?

内部类最直观的一个替代者,就是平时最常用的“组合”。只是需要一个内部组件的时候,用组合就好了,干嘛还非要用内部类呢?

作者给出的答案是:“多重继承”。

Each inner class can independently inherit from an implementation. Thus, the inner class is not limited by whether the outer class is already inheriting from an implementation.

在外部类已经继承了某个基类之后,内部类仍然可以继承别的类。不是简单多实现几个接口,而是实实在在地继承基类。

相较于组合,内部类的另一个优势在于,内部类不会像组合这样被强制和外部类一起被初始化。只有在我们需要用到的时候才去初始化内部类。这就是内部类的“可选组件”的地位,这点我们从上面“迭代器Iterator”的例子就能看出来。从设计的角度,内部类没有被逻辑绑定成外部类的一部分”IS-A”的关系。因此设计上更加灵活。

The point of creation of the inner-class object is not tied to the creation of the outer-class object.

另外,对于内部匿名类,它的好处在于简便,在一些类的使用次数非常有限,几乎是一次性的情况下,匿名类可以减轻工作量。

练习

Exercise 23

(4) Create an interface U with three methods. Create a class A with a method that produces a reference to a U by building an anonymous inner class. Create a second class B that contains an array of U. B should have one method that accepts and stores a reference to a U in the array, a second method that sets a reference in the array (specified by the method argument) to null, and a third method that moves through the array and calls the methods in U. In main( ), create a group of A objects and a single B. Fill the B with U references produced by the A objects. Use the B to call back into all the A objects. Remove some of the U references from the B.

结构很简单:

每个U实例都被标记上了生产它的A实例的ID和它自己的ID。这里体现出了匿名内部类作为闭包的性质:自带一个指向创建环境的指针,能访问创建者的全部信息。因此,每个U实例的三个方法根据创建环境ID参数的变化,都是全新的实现。这样的结构,很适合做工厂方法,批量生产特殊类。

匿名内部类接收现在可以接收非final参数了

练习的过程中,我做了个实验,想要验证关于匿名内部类只能访问final参数的问题。结果没想到Java 8已经取消了final参数的规定。但是看起来java编译器还是给每个传入匿名内部类的参数做了一份拷贝,因此匿名内部类对参数的操作,并不能改变外部参数。虽然保证了外部参数的安全性,但怎么觉得反而是个坑呢,不报错的隐蔽bug才最麻烦啊。

我实验的时候把容器B当成参数传给getU()函数,然后在匿名内部类中每次都重新初始化容器B。因此,按理最后容器B永远只能保存最后一个写入的U实例。但实际上最后容器B里的列表活得好好的。

    public U getU(){
        //Anonymous inner class
		//传入非Final容器B为参数
        return new U(B inputB){
			inputB=B.getB();	//每次都把B重新指向一个新的空容器
            public void uMethod1(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method1");}
            public void uMethod2(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method2");}
            public void uMethod3(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method3");}
            private int uId=uCount++;
        };
U.java
interface U {

    public void uMethod1();
    public void uMethod2();
    public void uMethod3();

}
A.java
//facotry of the object of type U
class A {
    //maintain a counter to generate the ID for each A
    private static int aCount=0;
    public static A getA(){return new A(aCount++);}

    public U getU(){
        //Anonymous inner class
		//闭包:匿名内部类自带创建者A的信息。
        return new U(){
            public void uMethod1(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method1");}
            public void uMethod2(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method2");}
            public void uMethod3(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method3");}
            private int uId=uCount++;
        };
    }

    private A(int inputAId){aId=inputAId;}


    private int aId;
    //maintain a counter to generate the ID for each U
    private int uCount=0;

}
B.java
//containor of created objects of type U
class B {

    public static B getB(){return new B();}
    //insert an U at the end of the uArray
    public void addU(U inputU){
        uArray.add(inputU);
    }
    //remove a U from uArray by their id
    public void removeU(int index){
        if(index<uArray.size() && uArray.get(index)!=null){
            uArray.remove(index);
        }
    }
    //pass through the U in uArray and call all three methods
    public void uIteration(){
        for(U u : uArray){
            u.uMethod1();
            u.uMethod2();
            u.uMethod3();
        }
    }
	//containor: List of U object
    private ArrayList<U> uArray=new ArrayList<U>();
Test.java
    public static void main(String[] args){
        //list of A
        A[] aArray=new A[3];
        for(int i=0;i<aArray.length;i++){
            aArray[i]=A.getA();
        }

        //B is list of U
        B b=B.getB();

        //A create U, insert into B
        for(int j=0;j<aArray.length;j++){
            for(int i=1;i<=j+1;i++){
                b.addU(aArray[j].getU());
            }
        }

        //show content in B
        b.uIteration();
        b.removeU(3);
        System.out.println("================================");
        b.uIteration();
    }

//每个A类实例都有ID,他们生产的U也有各自ID。每个U都是一个独一无二的类。
//output:
{A-0}.{U-0}.method1
{A-0}.{U-0}.method2
{A-0}.{U-0}.method3
{A-1}.{U-0}.method1
{A-1}.{U-0}.method2
{A-1}.{U-0}.method3
{A-1}.{U-1}.method1
{A-1}.{U-1}.method2
{A-1}.{U-1}.method3
{A-2}.{U-0}.method1
{A-2}.{U-0}.method2
{A-2}.{U-0}.method3
{A-2}.{U-1}.method1
{A-2}.{U-1}.method2
{A-2}.{U-1}.method3
{A-2}.{U-2}.method1
{A-2}.{U-2}.method2
{A-2}.{U-2}.method3
================================
{A-0}.{U-0}.method1
{A-0}.{U-0}.method2
{A-0}.{U-0}.method3
{A-1}.{U-0}.method1
{A-1}.{U-0}.method2
{A-1}.{U-0}.method3
{A-1}.{U-1}.method1
{A-1}.{U-1}.method2
{A-1}.{U-1}.method3
{A-2}.{U-1}.method1
{A-2}.{U-1}.method2
{A-2}.{U-1}.method3
{A-2}.{U-2}.method1
{A-2}.{U-2}.method2
{A-2}.{U-2}.method3